Skip to content

Refactor internal addon references to app/apps#6717

Merged
agners merged 6 commits intomainfrom
addon-to-app-in-internal-code
Apr 14, 2026
Merged

Refactor internal addon references to app/apps#6717
agners merged 6 commits intomainfrom
addon-to-app-in-internal-code

Conversation

@mdegat01
Copy link
Copy Markdown
Contributor

@mdegat01 mdegat01 commented Apr 8, 2026

Summary

This PR renames all internal code references from addon/addons to app/apps to make the codebase consistent with the user-facing rebranding done in #6696. The goal is to make the codebase clearer for future contributors by aligning internal naming with the new "apps" terminology.

What changed

  • Variable and argument names: addonapp, addonsapps
  • Function and method names: e.g. get_addon_logsget_app_logs
  • Class names: e.g. AddonApp, AddonManagerAppManager, DockerAddonDockerApp, all exception classes, check/fixup classes, etc.
  • Docstrings and comments
  • Test fixtures and parametrize strings

What was deliberately NOT changed

  • File and directory names (supervisor/addons/, addons/addon.py, etc.) — filesystem paths
  • External API JSON keys (e.g. "addons", "addon" in responses) — ATTR_APPS = "addons" keeps its string value
  • BackendAuthRequest(TypedDict).addon — used by HA Core auth API
  • Message(@attr.s).addon — used by the discovery protocol
  • extra_fields dict keys and message_template placeholders in exceptions — returned verbatim in API error responses
  • Error key strings (e.g. "addon_build_dockerfile_missing_error") — external API contract

Manual fix: backup/restore partial key remapping

The backup and restore partial API endpoints pass **body directly to do_backup_partial/do_restore_partial. The external API still accepts addons as the request key (since ATTR_APPS = "addons"), but the internal function parameters were renamed to apps. A remapping step was added before the **body expansion:

if ATTR_APPS in body:
    body["apps"] = body.pop(ATTR_APPS)

A test (test_restore_partial_with_addons_key) was also added to cover this path, which previously had no test.

Commit structure

The changes are split into three commits for reviewability:

  1. Docstrings and comments only — no runtime impact
  2. Code identifiers — variables, args, function/class names, string literals used as Python identifiers
  3. Backup API fix — manual key remapping + new test

Refactoring scripts used to perform the renames

The renames were performed in two passes using Python tokenizer-based scripts. Using the tokenize module (rather than regex) ensures identifiers, docstrings, f-string expressions, and import paths are all handled correctly. Both scripts are idempotent.

Pass 1 — docstrings and comments only

Run first to produce commit 1. Only touches COMMENT tokens and triple-quoted docstring STRING tokens; all code identifiers are left unchanged.

#!/usr/bin/env python3
"""Refactor addon → app in docstrings and comments ONLY (no code changes)."""
import io
import re
import sys
import tokenize as _tokenize
from pathlib import Path

CLASS_RENAMES = [
    ("AddonNotSupportedHomeAssistantVersionError", "AppNotSupportedHomeAssistantVersionError"),
    ("AddonPrePostBackupCommandReturnedError",     "AppPrePostBackupCommandReturnedError"),
    ("AddonBuildArchitectureNotSupportedError",    "AppBuildArchitectureNotSupportedError"),
    ("AddonBackupMetadataInvalidError",            "AppBackupMetadataInvalidError"),
    ("AddonBuildDockerfileMissingError",           "AppBuildDockerfileMissingError"),
    ("AddonBuildFailedUnknownError",               "AppBuildFailedUnknownError"),
    ("AddonNotSupportedArchitectureError",         "AppNotSupportedArchitectureError"),
    ("AddonNotSupportedMachineTypeError",          "AppNotSupportedMachineTypeError"),
    ("AddonNotSupportedWriteStdinError",           "AppNotSupportedWriteStdinError"),
    ("AddonConfigurationInvalidError",             "AppConfigurationInvalidError"),
    ("AddonBootConfigCannotChangeError",           "AppBootConfigCannotChangeError"),
    ("AddonNotSupportedError",                     "AppNotSupportedError"),
    ("AddonConfigurationError",                    "AppConfigurationError"),
    ("AddonNotRunningError",                       "AppNotRunningError"),
    ("AddonUnknownError",                          "AppUnknownError"),
    ("AddonPortConflict",                          "AppPortConflict"),
    ("AddonsJobError",                             "AppsJobError"),
    ("AddonsError",                                "AppsError"),
    ("APIAddonNotInstalled",                       "APIAppNotInstalled"),
    ("StoreAddonNotFoundError",                    "StoreAppNotFoundError"),
    ("StoreInvalidAddonRepo",                      "StoreInvalidAppRepo"),
    ("AddonLoggerAdapter",                         "AppLoggerAdapter"),
    ("AddonManager",                               "AppManager"),
    ("AddonsData",                                 "AppsData"),
    ("AddonBuild",                                 "AppBuild"),
    ("AddonModel",                                 "AppModel"),
    ("AddonStore",                                 "AppStore"),
    ("AddonOptions",                               "AppOptions"),
    ("AddonBackupMode",                            "AppBackupMode"),
    ("AddonBootConfig",                            "AppBootConfig"),
    ("AddonBoot",                                  "AppBoot"),
    ("AddonStartup",                               "AppStartup"),
    ("AddonStage",                                 "AppStage"),
    ("AddonState",                                 "AppState"),
    ("DockerAddon",                                "DockerApp"),
    ("CheckAddonPwned",                            "CheckAppPwned"),
    ("CheckDetachedAddonMissing",                  "CheckDetachedAppMissing"),
    ("CheckDetachedAddonRemoved",                  "CheckDetachedAppRemoved"),
    ("CheckDeprecatedArchAddon",                   "CheckDeprecatedArchApp"),
    ("CheckDeprecatedAddon",                       "CheckDeprecatedApp"),
    ("FixupAddonDisableBoot",                      "FixupAppDisableBoot"),
    ("FixupAddonExecuteRebuild",                   "FixupAppExecuteRebuild"),
    ("FixupAddonExecuteRemove",                    "FixupAppExecuteRemove"),
    ("FixupAddonExecuteRepair",                    "FixupAppExecuteRepair"),
    ("FixupAddonExecuteRestart",                   "FixupAppExecuteRestart"),
    ("FixupAddonExecuteStart",                     "FixupAppExecuteStart"),
    ("APIAddons",                                  "APIApps"),
    ("AnyAddon",                                   "AnyApp"),
    ("Addon",                                      "App"),
]

_DOC_SUBS: list[tuple[str, str]] = [
    ("Add-ons", "Apps"), ("Add-on", "App"),
    ("add-ons", "apps"), ("add-on", "app"),
    ("addons",  "apps"), ("Addons", "Apps"),
    ("addon",   "app"),  ("Addon",  "App"),
]

def _apply_class_renames_in_str(s: str) -> str:
    for old, new in CLASS_RENAMES:
        s = s.replace(old, new)
    return s

def _apply_doc_replacements(s: str) -> str:
    s = _apply_class_renames_in_str(s)
    for old, new in _DOC_SUBS:
        s = re.sub(r"\b" + re.escape(old) + r"\b", new, s)
    return s

def _is_docstring(s: str) -> bool:
    bare = s.lstrip("rRbBuUfF")
    return bare.startswith('"""') or bare.startswith("'''")

def process_file(path: Path) -> bool:
    original = path.read_text(encoding="utf-8")
    try:
        tokens = list(_tokenize.generate_tokens(io.StringIO(original).readline))
    except _tokenize.TokenError:
        return False

    line_offsets: list[int] = [0]
    for line in original.splitlines(keepends=True):
        line_offsets.append(line_offsets[-1] + len(line))

    def to_offset(row: int, col: int) -> int:
        return line_offsets[row - 1] + col

    edits: list[tuple[int, int, str]] = []
    for ttype, tstring, tstart, tend, _ in tokens:
        s_off = to_offset(*tstart)
        e_off = to_offset(*tend)
        if ttype == _tokenize.COMMENT:
            new_s = _apply_doc_replacements(tstring)
            if new_s != tstring:
                edits.append((s_off, e_off, new_s))
        elif ttype == _tokenize.STRING and _is_docstring(tstring):
            new_s = _apply_doc_replacements(tstring)
            if new_s != tstring:
                edits.append((s_off, e_off, new_s))

    if not edits:
        return False
    edits.sort(key=lambda e: e[0], reverse=True)
    chars = list(original)
    for start, end, new in edits:
        chars[start:end] = list(new)
    result = "".join(chars)
    if result != original:
        path.write_text(result, encoding="utf-8")
        return True
    return False

def main():
    root = Path(sys.argv[1]) if len(sys.argv) > 1 else Path(".")
    for sr in [root / "supervisor", root / "tests"]:
        for py_file in sorted(sr.rglob("*.py")) if sr.exists() else []:
            if "__pycache__" not in py_file.parts:
                process_file(py_file)

if __name__ == "__main__":
    main()

Pass 2 — code identifiers (run after pass 1)

Handles NAME tokens (identifiers), f-string expressions, and non-docstring string literals. Import module paths are protected. External interface fields (TypedDict, @attr.s) are detected and reverted after the token pass.

#!/usr/bin/env python3
"""Refactor addon → app in internal Python code using Python's tokenizer."""
import io
import re
import sys
import tokenize as _tokenize
from pathlib import Path

CLASS_RENAMES = [
    ("AddonNotSupportedHomeAssistantVersionError", "AppNotSupportedHomeAssistantVersionError"),
    ("AddonPrePostBackupCommandReturnedError",     "AppPrePostBackupCommandReturnedError"),
    ("AddonBuildArchitectureNotSupportedError",    "AppBuildArchitectureNotSupportedError"),
    ("AddonBackupMetadataInvalidError",            "AppBackupMetadataInvalidError"),
    ("AddonBuildDockerfileMissingError",           "AppBuildDockerfileMissingError"),
    ("AddonBuildFailedUnknownError",               "AppBuildFailedUnknownError"),
    ("AddonNotSupportedArchitectureError",         "AppNotSupportedArchitectureError"),
    ("AddonNotSupportedMachineTypeError",          "AppNotSupportedMachineTypeError"),
    ("AddonNotSupportedWriteStdinError",           "AppNotSupportedWriteStdinError"),
    ("AddonConfigurationInvalidError",             "AppConfigurationInvalidError"),
    ("AddonBootConfigCannotChangeError",           "AppBootConfigCannotChangeError"),
    ("AddonNotSupportedError",                     "AppNotSupportedError"),
    ("AddonConfigurationError",                    "AppConfigurationError"),
    ("AddonNotRunningError",                       "AppNotRunningError"),
    ("AddonUnknownError",                          "AppUnknownError"),
    ("AddonPortConflict",                          "AppPortConflict"),
    ("AddonsJobError",                             "AppsJobError"),
    ("AddonsError",                                "AppsError"),
    ("APIAddonNotInstalled",                       "APIAppNotInstalled"),
    ("StoreAddonNotFoundError",                    "StoreAppNotFoundError"),
    ("StoreInvalidAddonRepo",                      "StoreInvalidAppRepo"),
    ("AddonLoggerAdapter",                         "AppLoggerAdapter"),
    ("AddonManager",                               "AppManager"),
    ("AddonsData",                                 "AppsData"),
    ("AddonBuild",                                 "AppBuild"),
    ("AddonModel",                                 "AppModel"),
    ("AddonStore",                                 "AppStore"),
    ("AddonOptions",                               "AppOptions"),
    ("AddonBackupMode",                            "AppBackupMode"),
    ("AddonBootConfig",                            "AppBootConfig"),
    ("AddonBoot",                                  "AppBoot"),
    ("AddonStartup",                               "AppStartup"),
    ("AddonStage",                                 "AppStage"),
    ("AddonState",                                 "AppState"),
    ("DockerAddon",                                "DockerApp"),
    ("CheckAddonPwned",                            "CheckAppPwned"),
    ("CheckDetachedAddonMissing",                  "CheckDetachedAppMissing"),
    ("CheckDetachedAddonRemoved",                  "CheckDetachedAppRemoved"),
    ("CheckDeprecatedArchAddon",                   "CheckDeprecatedArchApp"),
    ("CheckDeprecatedAddon",                       "CheckDeprecatedApp"),
    ("FixupAddonDisableBoot",                      "FixupAppDisableBoot"),
    ("FixupAddonExecuteRebuild",                   "FixupAppExecuteRebuild"),
    ("FixupAddonExecuteRemove",                    "FixupAppExecuteRemove"),
    ("FixupAddonExecuteRepair",                    "FixupAppExecuteRepair"),
    ("FixupAddonExecuteRestart",                   "FixupAppExecuteRestart"),
    ("FixupAddonExecuteStart",                     "FixupAppExecuteStart"),
    ("APIAddons",                                  "APIApps"),
    ("AnyAddon",                                   "AnyApp"),
    ("Addon",                                      "App"),
]

EXACT_NAME_MAP: dict[str, str] = {old: new for old, new in CLASS_RENAMES}
EXACT_NAME_MAP.update({
    "ATTR_ADDONS_CUSTOM_LIST":   "ATTR_APPS_CUSTOM_LIST",
    "ATTR_ADDONS_REPOSITORIES":  "ATTR_APPS_REPOSITORIES",
    "ATTR_ADDONS":               "ATTR_APPS",
    "ATTR_ADDON":                "ATTR_APP",
    "ADDON_UPDATE_CONDITIONS":   "APP_UPDATE_CONDITIONS",
    "sys_addons":                "sys_apps",
    "path_extern_addons_local":  "path_extern_apps_local",
    "path_extern_addons_data":   "path_extern_apps_data",
    "path_extern_addon_configs": "path_extern_app_configs",
})

_COMPONENT_MAP = {"addon": "app", "Addon": "App", "addons": "apps", "Addons": "Apps"}

def _rename_snake_components(name: str) -> str:
    return "_".join(_COMPONENT_MAP.get(p, p) for p in name.split("_"))

def _rename_name_token(name: str, in_import: bool) -> str:
    if name in EXACT_NAME_MAP:
        return EXACT_NAME_MAP[name]
    if in_import:
        return name
    return _rename_snake_components(name)

_DOC_SUBS: list[tuple[str, str]] = [
    ("Add-ons", "Apps"), ("Add-on", "App"),
    ("add-ons", "apps"), ("add-on", "app"),
    ("addons",  "apps"), ("Addons", "Apps"),
    ("addon",   "app"),  ("Addon",  "App"),
]

def _apply_class_renames_in_str(s: str) -> str:
    for old, new in CLASS_RENAMES:
        s = s.replace(old, new)
    return s

def _is_fstring(s: str) -> bool:
    return s[:1] in ("f", "F") or (len(s) > 1 and s[:2].lower() in ("rf", "fr"))

def _process_fstring_exprs(s: str) -> str:
    def replace_expr(m: re.Match) -> str:
        expr = m.group(1)
        renamed = re.sub(r"\b([A-Za-z_]\w*)\b", lambda x: _rename_name_token(x.group(), False), expr)
        return "{" + renamed + "}"

def process_file(path: Path) -> bool:
    original = path.read_text(encoding="utf-8")
    try:
        tokens = list(_tokenize.generate_tokens(io.StringIO(original).readline))
    except _tokenize.TokenError:
        return False

    line_offsets: list[int] = [0]
    for line in original.splitlines(keepends=True):
        line_offsets.append(line_offsets[-1] + len(line))

    def to_offset(row: int, col: int) -> int:
        return line_offsets[row - 1] + col

    attr_classes: set[str] = set()
    for m in re.finditer(r"@attr\.s[^\n]*\nclass\s+(\w+)", original):
        attr_classes.add(m.group(1))

    edits: list[tuple[int, int, str]] = []
    in_import = False
    after_import_kw = False

    for ttype, tstring, tstart, tend, _ in tokens:
        s_off = to_offset(*tstart)
        e_off = to_offset(*tend)

        if ttype == _tokenize.NAME:
            if tstring == "from" and not in_import:
                in_import = True
                after_import_kw = False
            elif tstring == "import" and in_import and not after_import_kw:
                after_import_kw = True
                in_import = False
            new = _rename_name_token(tstring, in_import)
            if new != tstring:
                edits.append((s_off, e_off, new))

        elif ttype == _tokenize.STRING:
            bare = tstring.lstrip("rRbBuUfF")
            is_doc = bare.startswith('"""') or bare.startswith("'''")
            if not is_doc:  # docstrings already handled by pass 1
                if _is_fstring(tstring):
                    new_s = _apply_class_renames_in_str(tstring)
                    new_s = _process_fstring_exprs(new_s)
                else:
                    new_s = _apply_class_renames_in_str(tstring)
                if new_s != tstring:
                    edits.append((s_off, e_off, new_s))

        elif ttype in (_tokenize.NEWLINE, _tokenize.NL):
            if in_import:
                in_import = False
            after_import_kw = False

    if not edits:
        result = original
    else:
        edits.sort(key=lambda e: e[0], reverse=True)
        chars = list(original)
        for start, end, new in edits:
            chars[start:end] = list(new)
        result = "".join(chars)

    # Post-processing: revert external interface fields
    result = re.sub(
        r"(class BackendAuthRequest\(TypedDict\):[\s\S]*?\n    )app(\s*:)",
        r"\1addon\2", result,
    )
    result = re.sub(r"(BackendAuthRequest\([^)]*,\s*)app(=)", r"\1addon=", result)
    for cls_name in attr_classes:
        def revert_attr_fields(m: re.Match) -> str:
            body = m.group(0)
            body = re.sub(r"\n    app(\s*:)", r"\n    addon\1", body)
            body = re.sub(r"\n    apps(\s*:)", r"\n    addons\1", body)
            return body
        result = re.sub(
            rf"@attr\.s[^\n]*\nclass {re.escape(cls_name)}[\s\S]*?(?=\n\S|\Z)",
            revert_attr_fields, result,
        )
    result = re.sub(r'\bmessage\.app\b', 'message.addon', result)
    result = re.sub(r'\bMessage\(app=', 'Message(addon=', result)
    result = re.sub(r'(["\']/(?:store/)?addons/)\{addon\}', r'\1{app}', result)
    result = re.sub(r'match_info\["addon"\]', 'match_info["app"]', result)
    result = re.sub(r"match_info\['addon'\]", "match_info['app']", result)
    result = re.sub(r'match_info\.get\("addon"', 'match_info.get("app"', result)
    result = re.sub(r"match_info\.get\('addon'", "match_info.get('app'", result)
    result = re.sub(r'"addon_slug"', '"app_slug"', result)
    result = re.sub(r"'addon_slug'", "'app_slug'", result)
    result = re.sub(r'"addon_token"', '"app_token"', result)
    result = re.sub(r"'addon_token'", "'app_token'", result)
    result = re.sub(r'"install_addon_(\w+)"', r'"install_app_\1"', result)
    result = re.sub(r"'install_addon_(\w+)'", r"'install_app_\1'", result)
    def _rename_patch_obj_attr(m: re.Match) -> str:
        prefix, attr_str = m.group(1), m.group(2)
        return f'{prefix}"{_rename_snake_components(attr_str)}"'
    result = re.sub(
        r'(patch\.object\([^,]+,\s*)"([a-z][a-z0-9_]*)"',
        _rename_patch_obj_attr, result,
    )

    if result != original:
        path.write_text(result, encoding="utf-8")
        return True
    return False

def main():
    root = Path(sys.argv[1]) if len(sys.argv) > 1 else Path(".")
    for sr in [root / "supervisor", root / "tests"]:
        for py_file in sorted(sr.rglob("*.py")) if sr.exists() else []:
            if "__pycache__" not in py_file.parts:
                process_file(py_file)

if __name__ == "__main__":
    main()

@mdegat01 mdegat01 force-pushed the addon-to-app-in-internal-code branch from 5530a82 to 8e1e500 Compare April 8, 2026 18:15
@mdegat01
Copy link
Copy Markdown
Contributor Author

mdegat01 commented Apr 8, 2026

So.... I'm open to suggestions on how to break this enormous PR up lol. I considered asking the AI to go one module at a time but it would still cascade a ton. Since it would then have to change references elsewhere and end up making a lot of sections of confusing code that had half apps references and half addons references.

I figured I'd start with what it got as long as the tests passed and we'd decide where to go from there.

@mdegat01 mdegat01 added the refactor A code change that neither fixes a bug nor adds a feature label Apr 8, 2026
@sairon
Copy link
Copy Markdown
Member

sairon commented Apr 9, 2026

I figured I'd start with what it got as long as the tests passed and we'd decide where to go from there.

I agree. It's indeed enormous but on the other hand the changes are quite straight-forward to review - I think it's fine to do it in one shot rather that doing it in smaller chunks. I can imagine the changes could have effect out of the scope of a single module that you'd not realize when reviewing what's basically a s/addon/app/g diff but that's what the tests should catch. In fact, systematically replacing everything is then better than replacing just a part.

Copy link
Copy Markdown
Member

@sairon sairon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking into the changes a bit deeper, there could be done some improvements for easier review (which should be easy to be performed by AI). Ideally splitting it into several commits - hunks that only change comments and docstrings (there are tons of such changes that we don't care about much) in one changeset, and changes in the code itself in another - on those we can better focus during the review.

I also don't agree this should be left out:

File and directory names (supervisor/addons/, addons/addon.py, etc.) — filesystem paths

AFAIK the module/package names and paths are only used internally by Supervisor - we don't need to fulfill any contract by keeping the names the same there.

@home-assistant home-assistant bot marked this pull request as draft April 9, 2026 09:09
@home-assistant
Copy link
Copy Markdown

home-assistant bot commented Apr 9, 2026

Please take a look at the requested changes, and use the Ready for review button when you are done, thanks 👍

Learn more about our pull request process.

@mdegat01
Copy link
Copy Markdown
Contributor Author

mdegat01 commented Apr 9, 2026

AFAIK the module/package names and paths are only used internally by Supervisor - we don't need to fulfill any contract by keeping the names the same there.

@sairon we do actually 🙈 But only in one place, resolution manager. Here's where the slugs of checks, evaluations and fixups come from currently:

@property
def slug(self) -> str:
"""Return the check slug."""
return self.__class__.__module__.rsplit(".", maxsplit=1)[-1]

@property
def slug(self) -> str:
"""Return the check slug."""
return self.__class__.__module__.rsplit(".", maxsplit=1)[-1]

@property
def slug(self) -> str:
"""Return the check slug."""
return self.__class__.__module__.rsplit(".", maxsplit=1)[-1]

And these do definitely make their way into the API contract.

The fix is quite simple - make this field abstract and have each class override it with a hard-coded value. But since coding changes were required for this one (not just find and replace) and it was quite easy to carve this particular bit off into a separate PR I figured I'd leave it for follow-up.

@mdegat01 mdegat01 force-pushed the addon-to-app-in-internal-code branch from 8e1e500 to c51bb19 Compare April 9, 2026 16:27
@mdegat01
Copy link
Copy Markdown
Contributor Author

mdegat01 commented Apr 9, 2026

@sairon ok PR split into 3 commits:

  1. docstrings and comments only commit
  2. all other changes made by the script to runnable code
  3. post-script fix to partial backup and restore APIs to handle postbody mapping

What it actually did was took a chunk out of its original script and made a new docstrings and comments only one. Then it just ran the original one as is since its idempotent anyway. I had it add the new script to the details in the expand pane as well.

@mdegat01 mdegat01 force-pushed the addon-to-app-in-internal-code branch from c51bb19 to 15ada2c Compare April 9, 2026 16:39
@mdegat01 mdegat01 marked this pull request as ready for review April 9, 2026 16:39
@home-assistant home-assistant bot requested a review from sairon April 9, 2026 16:39
Copy link
Copy Markdown
Member

@sairon sairon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, it's still PITA to review but at least the change traceability is better 🙈 I can't say I've review it thoroughly but it I don't see anything strange, the changes are generally mechanical and tests pass, so let's go with that 👍

mdegat01 and others added 3 commits April 10, 2026 17:56
Updates all docstrings and inline comments across supervisor/ and
tests/ to use the new app/apps terminology. No runtime behaviour
is changed by this commit.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Renames all internal Python identifiers from addon/addons to app/apps:
- Variable and argument names
- Function and method names
- Class names (Addon→App, AddonManager→AppManager, DockerAddon→DockerApp,
  all exception, check, and fixup classes, etc.)
- String literals used as Python identifiers (pytest fixtures,
  parametrize param names, patch.object attribute strings,
  URL route match_info keys)

External API contracts are preserved: JSON keys, error codes,
discovery protocol fields, TypedDict/attr.s field names.
Import module paths (supervisor/addons/) are also unchanged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The external API accepts `addons` as the request body key (since
ATTR_APPS = "addons"), but do_backup_partial and do_restore_partial
now take an `apps` parameter after the rename. The **body expansion
in both endpoints would pass `addons=...` causing a TypeError.

Remap the key before expansion in both backup_partial and
restore_partial:

    if ATTR_APPS in body:
        body["apps"] = body.pop(ATTR_APPS)

Also adds test_restore_partial_with_addons_key to verify the restore
path correctly receives apps= when addons is passed in the request
body. This path had no existing test coverage.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mdegat01 mdegat01 force-pushed the addon-to-app-in-internal-code branch from 15ada2c to cad44b2 Compare April 10, 2026 18:05
@agners agners marked this pull request as draft April 10, 2026 18:17
@agners
Copy link
Copy Markdown
Member

agners commented Apr 10, 2026

Converting to draft for now since we want to do a release which includes #6590 before merging this.

@agners agners marked this pull request as ready for review April 14, 2026 10:28
@home-assistant home-assistant bot requested a review from sairon April 14, 2026 10:28
@home-assistant home-assistant bot marked this pull request as draft April 14, 2026 13:47
Co-authored-by: Stefan Agner <stefan@agner.ch>
@mdegat01 mdegat01 marked this pull request as ready for review April 14, 2026 14:30
@home-assistant home-assistant bot requested a review from agners April 14, 2026 14:30
Copy link
Copy Markdown
Member

@agners agners left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, also runtime tested locally, worked fine 👍 .

@agners agners merged commit ba8c499 into main Apr 14, 2026
21 checks passed
@agners agners deleted the addon-to-app-in-internal-code branch April 14, 2026 14:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cla-signed refactor A code change that neither fixes a bug nor adds a feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants